import Foundation
import Clibsodium

public struct PWHash {
    public let SaltBytes = Int(crypto_pwhash_saltbytes())
    public let StrBytes = Int(crypto_pwhash_strbytes()) - (1 as Int)
    public let StrPrefix = String(validatingUTF8: crypto_pwhash_strprefix())
    public let OpsLimitInteractive = Int(crypto_pwhash_opslimit_interactive())
    public let OpsLimitModerate = Int(crypto_pwhash_opslimit_moderate())
    public let OpsLimitSensitive = Int(crypto_pwhash_opslimit_sensitive())
    public let MemLimitInteractive = Int(crypto_pwhash_memlimit_interactive())
    public let MemLimitModerate = Int(crypto_pwhash_memlimit_moderate())
    public let MemLimitSensitive = Int(crypto_pwhash_memlimit_sensitive())
}

extension PWHash {
    public enum Alg {
        case Default
        case Argon2I13
        case Argon2ID13
    }
}

extension PWHash.Alg {
    var id: Int32 {
        switch self {
        case .Default:    return crypto_pwhash_alg_default()
        case .Argon2I13:  return crypto_pwhash_alg_argon2i13()
        case .Argon2ID13: return crypto_pwhash_alg_argon2id13()
        }
    }
}

extension PWHash {
    /**
     Generates an ASCII encoded string, which includes:

     - the result of the memory-hard, CPU-intensive Argon2 password hashing function applied to the password `passwd`
     - the automatically generated salt used for the previous computation
     - the other parameters required to verify the password, including the algorithm identifier, its version, opslimit and memlimit.

     The output string includes only ASCII characters and can be safely stored into SQL databases and other data stores. No extra information has to be stored in order to verify the password.

     - Parameter passwd: The password data to hash.
     - Parameter opsLimit: Represents a maximum amount of computations to perform. Raising this number will make the function require more CPU cycles to compute a key.
     - Parameter memLimit: The maximum amount of RAM that the function will use, in bytes.

     - Returns: The generated string.
     */
    public func str(passwd: Bytes, opsLimit: Int, memLimit: Int) -> String? {
        var output = Bytes(count: StrBytes).map(Int8.init)
        let passwd = passwd.map(Int8.init)

        guard .SUCCESS == crypto_pwhash_str(
            &output,
            passwd, UInt64(passwd.count),
            UInt64(opsLimit),
            size_t(memLimit)
        ).exitCode else { return nil }

        return String(cString: output)
    }

    /**
     Verifies that the password str is a valid password verification string (as generated by `str(passwd: Bytes, opslimit: Int, memLimit: Int)` for `passwd`.

     - Parameter hash: The password hash string to verify.
     - Parameter passwd: The password data to verify.

     - Returns: `true` if the verification succeeds.
     */
    public func strVerify(hash: String, passwd: Bytes) -> Bool {
        let hashBytes = Bytes((hash + "\0").utf8).map(Int8.init)
        let passwd = passwd.map(Int8.init)

        return .SUCCESS == crypto_pwhash_str_verify(
            hashBytes,
            passwd, UInt64(passwd.count)
        ).exitCode
    }

    /**
     Checks that a string previously hashed password matches the current algorithm and parameters

     - Parameter hash: The password hash string to check.
     - Parameter opsLimit: Represents a maximum amount of computations to perform. Raising this number will make the function require more CPU cycles to compute a key.
     - Parameter memLimit: The maximum amount of RAM that the function will use, in bytes.

     - Returns: `true` if the password hash should be updated.
     */
    public func strNeedsRehash(hash: String, opsLimit: Int, memLimit: Int) -> Bool {
        let hashBytes = Bytes((hash + "\0").utf8).map(Int8.init)
        return .SUCCESS != crypto_pwhash_str_needs_rehash(
            hashBytes,
            UInt64(opsLimit),
            size_t(memLimit)
        ).exitCode
    }
}

extension PWHash {
    /**
     Derives a key from a password and a salt using the Argon2 password hashing function.

     Keep in mind that in order to produce the same key from the same password, the same salt, and the same values for opslimit and memlimit have to be used. Therefore, these parameters have to be stored for each user.

     - Parameter outputLength: Desired length of the derived key.  Should be at least 16 (128 bits)
     - Parameter passwd: The password data to hash.
     - Parameter salt: Unpredicatable salt data.  Must have a fixed length of `SaltBytes`.
     - Parameter opsLimit: Represents a maximum amount of computations to perform. Raising this number will make the function require more CPU cycles to compute a key.
     - Parameter memLimit: The maximum amount of RAM that the function will use, in bytes.
     - Parameter alg: The algorithm identifier (`.Default`, `.Argon2I13`, `.Argon2ID13`).

     - Returns: The derived key data.
     */
    public func hash(outputLength: Int, passwd: Bytes, salt: Bytes, opsLimit: Int, memLimit: Int, alg: Alg = .Default) -> Bytes? {
        guard salt.count == SaltBytes else { return nil }
        var output = Bytes(count: outputLength)
        let passwd = passwd.map(Int8.init)

        guard .SUCCESS == crypto_pwhash(
            &output, UInt64(outputLength),
            passwd, UInt64(passwd.count),
            salt,
            UInt64(opsLimit),
            size_t(memLimit),
            alg.id
        ).exitCode else { return nil }

        return output
    }
}
